#ifndef STATSxx_MACHINE_LEARNING_Ensemble_HPP
#define STATSxx_MACHINE_LEARNING_Ensemble_HPP

// STL
#include <string>                                // std::string
#include <vector>                                // std::vector<>

// Boost
#include <boost/serialization/serialization.hpp> // boost::serialization::
#include <boost/serialization/vector.hpp>        // serialize std::vector<>

// jScience
#include "jScience/linalg.hpp"                   // Matrix<>

// stats++
#include "statsxx/machine_learning/Learner.hpp"  // Learner


//=========================================================
// ENSEMBLE
//=========================================================
//
// NOTE: Templating this class seems necessary to use Boost serialization efficiently.
// NOTE: ... Without knowing the type of learner, for example, it would be impossible to deserialize.
//
// TODO: NOTE: ... This is fine for homogeneous ensembles, but (obviously) would not handle heterogeneous ones.
//
// TODO: NOTE: Need to think a further about the best way to do this:
//
//     - Store also the type of each learner
//     - Allow combination of ensembles (each homogeneous) --- the former may be more general, but then can't efficiently use <boost/serialization/vector.hpp>, etc.
//
// -----
//
// TODO: NOTE: Boosting currently only works for two-class classification.
//
// -----
//
// TODO: NOTE: Need to think about how best to represent the data --- right now it is Matrix<> for a collection of data, and std::vector<> for an individual input or output.
//
// TODO: NOTE: ... This leads to some additional conversions of data [see ::optimize_BMC(), for example].
//
// TODO: NOTE: ... This is an issue that extends throughout stats++.
//
// -----
//
// TODO: NOTE: All of the Ensemble subroutines are *.CPP files, despite them being templated.
//
// TODO: NOTE: ... Think about changing to *.TPP.
//
template<typename T>
class Ensemble
{

public:

    bool                is_classif;

    int                 ni;
    int                 no;

    std::vector<T>      learners;
    std::vector<double> w;

    //---------------------------------------------------------
    // CONSTRUCTORS/DESTRUCTOR
    //---------------------------------------------------------

    Ensemble();
    Ensemble(
             const bool _is_classif,
             // -----
             const int  _ni,
             const int  _no
             );

    ~Ensemble();

    //---------------------------------------------------------
    // CREATION
    //---------------------------------------------------------

    void add_learner(
                     const T &_learner
                     );


    //---------------------------------------------------------
    // TRAINING
    //---------------------------------------------------------

    void train_bootstrap(
                         const Matrix<double> &X_in,
                         const Matrix<double> &X_out
                         );

    // *****
    // TODO: NOTE: Add train_CV() with the following NOTEs:
    //
    // NOTE: Two prominent methods (that I can think of) are: (1) bootstrapping; (2) cross-validation.
    //
    // NOTE: ... (2) is used in the classic paper:
    //
    //     A. Krogh and J. Vedelsby, "Neural Network Ensembles, Cross Validation, and Active Learning"
    //
    // NOTE: ... (Direct) cross-validation will not work well however in the limits of very few or many ensembles.
    //
    // NOTE: For the (example) reason(s) above, (only) (1) is implemented below.
    // *****

    void train_Real_AdaBoost(
                             const Matrix<double> &X_in,
                             const Matrix<double> &X_out
                             );

    //---------------------------------------------------------
    // OPTIMIZATION
    //---------------------------------------------------------

    void optimize_Ensemble_gen_err(
                                   const Matrix<double> &X_in,
                                   const Matrix<double> &X_out,
                                   // -----
                                   const double          eps,
                                   // -----
                                   const std::string     prefix
                                   );

    void optimize_BMA(
                      const Matrix<double> &X_in,
                      const Matrix<double> &X_out,
                      // -----
                      const std::string     prefix
                      );

    void optimize_BMC(
                      const Matrix<double> &X_in,
                      const Matrix<double> &X_out,
                      // -----
                      const int             N,
                      // -----
                      const std::string     prefix
                      );

    //---------------------------------------------------------
    // UTILITY
    //---------------------------------------------------------

    std::vector<double> evaluate(
                                 const std::vector<double> &input
                                 );

    void output_weights(
                        const std::string prefix
                        );


private:

    bool _evaluate_Boost;

    //---------------------------------------------------------
    // CREATION
    //---------------------------------------------------------

    void init_weights();

    //---------------------------------------------------------
    // UTILITY
    //---------------------------------------------------------

    std::vector<double> evaluate_Boost(
                                       const std::vector<double> &input
                                       );


    Matrix<double> combine_output(
                                  const std::vector<Matrix<double>> &X_i
                                  );

    //---------------------------------------------------------
    // (Boost) SERIALIZATION
    //---------------------------------------------------------

    friend class boost::serialization::access;

    template<class Archive>
    void serialize(
                   Archive            &ar,
                   const unsigned int  version
                   )
    {
        ar & this->is_classif;

        ar & this->ni;
        ar & this->no;

        ar & this->learners;
        ar & this->w;

        ar & this->_evaluate_Boost;
    }

};

#include "statsxx/machine_learning/Ensemble/add_learner.cpp"
#include "statsxx/machine_learning/Ensemble/combine_output.cpp"
#include "statsxx/machine_learning/Ensemble/Ensemble.cpp"
#include "statsxx/machine_learning/Ensemble/evaluate.cpp"
#include "statsxx/machine_learning/Ensemble/evaluate_Boost.cpp"
#include "statsxx/machine_learning/Ensemble/init_weights.cpp"
#include "statsxx/machine_learning/Ensemble/optimize_BMA.cpp"
#include "statsxx/machine_learning/Ensemble/optimize_BMC.cpp"
#include "statsxx/machine_learning/Ensemble/optimize_Ensemble_gen_err.cpp"
#include "statsxx/machine_learning/Ensemble/output_weights.cpp"
#include "statsxx/machine_learning/Ensemble/train_bootstrap.cpp"
#include "statsxx/machine_learning/Ensemble/train_Real_AdaBoost.cpp"


#endif
